#!/usr/bin/env python
# -*- coding:utf-8 -*-

# 八面体
# 1.先安装包
import pyrr  # NumPy的3D函数库
import svgwrite # svg图形处理库

# 2. 定义3D图生成环境
# 定义几个类设置好3维图基础环境.
import numpy

from typing import NamedTuple,Callable,Sequence

class Viewport(NamedTuple):
    # Viewport: 矩形图范围
    minx: float =  -0.5
    miny: float = -0.5
    width: float = 1.0
    height: float = 1.0

class Camera(NamedTuple):
    # Camera: 包括视图矩阵和投影矩阵.
    view: numpy.ndarray
    projection: numpy.ndarray

class Mesh(NamedTuple):
    # mesh: svg矢量图所需的网格表面矩阵,着色器和样式字典.
    faces: numpy.ndarray
    style: dict = None
    shader: Callable[[int,float],dict] = None

class Scene(NamedTuple):
    meshes: Sequence[Mesh]


# 3. 生成八面体每个定点的数据
def octahedron():
    f = sqrt(2.0) / 2.0
    verts = numpy.float32([(0,-1,0),(-f,0,f),(f,0,f),(f,0,-f),(-f,0,-f),(0,1,0)])
    triangles = numpy.int32([(0,2,1),(0,3,2),(0,4,3),(0,1,4),(5,1,2),(5,2,3),(5,3,4),(5,4,1)])
    return verts[triangles]

# 4. pyrr包渲染
# 利用pyrr 3维库渲染上面的原始数据,最后生成svg矢量图.
import pyrr

projection_matrix = pyrr.matrix44.create_perspective_projection(fovy=25,aspect=1,near=10,far=100)

view_matrix = pyrr.matrix44.create_look_at(eye=[25,-20,60],target=[0,0,0],up=[0,1,0])

camera = Camera(view_matrix,projection_matrix)

style = dict(
    fill = 'white',fill_opacity= '0.75',
    stroke = 'black',stroke_linejoin = 'round',stroke_width='0.005')

mesh = Mesh(15.0 * octahedron(),style=style)
view = View(camera,Scene([mesh]))
Engine([view]).render('octahedron.svg')


# 5. 生成svg
# 上面最后一步生成svg图形,需要调用下面的Engine函数.
import numpy
import svgwrite

class Engine:
    
    def __init__(self,views):
        self.views = views 

    def render(self,filename,size=(512,512),viewBox='-0.5 -0.5 1.0 1.0'):
        drawing = svgwrite.Drawing(filename,size,viewBox=viewBox)
        for view in self.views:
            projection = numpy.dot(view.camera.view,view.camera.projection)
            for mesh in view.scene.meshes:
                drawing.add(self._create_group(drawing,projection,view.viewport,mesh))
            drawing.save()

    def _create_group(self,drawing,projection,viewport, mesh):
        faces = mesh.faces 
        shader = mesh.shader or (lambda face_index,winding: {})
        default_style = mesh.style or {}

        ones = numpy.ones(faces.shape[:2] + (1,))
        faces = numpy.dstack([faces,ones])
        faces = numpy.dot(faces,projection)

        faces[:,:,:3] /= faces[:,:,3:4]
        faces = faces[:,:,:3]

        faces[:,:,0:1]  = (1.0 + faces[:,:,0:1]) * viewport.width / 2
        faces[:,:,1:2] = (1.0 - faces[:,:,1:2]) * viewport.height / 2
        faces[:,:,0:1] += viewport.minx 
        faces[:,:,1:2] += viewport.miny

        z_centroids = -np.sum(faces[:,:,2], axis=1)
        for face_index in range(len(z_centroids)):
            z_centroids[face_index] /= len(faces[face_index])

        face_indices = np.argsort(z_centroids)
        faces = faces[face_indices]

        

        group = drawing.g(**default_style)
        face_index = 0
        for face in faces:
            p0,p1,p2 = face[0],face[1],face[2]
            winding = pyrr.vector3.cross(p2 - p0, p1 - p0)[2]
            style = shader(face_indices[face_index],winding)
            if style != None:
                group.add(drawing.polygon(face[:,0:2],**style))
            face_index = face_index + 1

        return group
